/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kika.ddmlib;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.ParseException;

/**
 * Describes the types and locations of objects in a segment of a heap.
 */
public final class HeapSegment implements Comparable<HeapSegment> {

	/**
	 * Describes an object/region encoded in the HPSG data.
	 */
	public static class HeapSegmentElement implements
			Comparable<HeapSegmentElement> {

		/*
		 * Solidity values, which must match the values in the HPSG data.
		 */

		/** The element describes a free block. */
		public static int SOLIDITY_FREE = 0;

		/** The element is strongly-reachable. */
		public static int SOLIDITY_HARD = 1;

		/** The element is softly-reachable. */
		public static int SOLIDITY_SOFT = 2;

		/** The element is weakly-reachable. */
		public static int SOLIDITY_WEAK = 3;

		/** The element is phantom-reachable. */
		public static int SOLIDITY_PHANTOM = 4;

		/** The element is pending finalization. */
		public static int SOLIDITY_FINALIZABLE = 5;

		/** The element is not reachable, and is about to be swept/freed. */
		public static int SOLIDITY_SWEEP = 6;

		/** The reachability of the object is unknown. */
		public static int SOLIDITY_INVALID = -1;

		/*
		 * Kind values, which must match the values in the HPSG data.
		 */

		/** The element describes a data object. */
		public static int KIND_OBJECT = 0;

		/** The element describes a class object. */
		public static int KIND_CLASS_OBJECT = 1;

		/** The element describes an array of 1-byte elements. */
		public static int KIND_ARRAY_1 = 2;

		/** The element describes an array of 2-byte elements. */
		public static int KIND_ARRAY_2 = 3;

		/** The element describes an array of 4-byte elements. */
		public static int KIND_ARRAY_4 = 4;

		/** The element describes an array of 8-byte elements. */
		public static int KIND_ARRAY_8 = 5;

		/** The element describes an unknown type of object. */
		public static int KIND_UNKNOWN = 6;

		/** The element describes a native object. */
		public static int KIND_NATIVE = 7;

		/** The object kind is unknown or unspecified. */
		public static int KIND_INVALID = -1;

		/**
		 * A bit in the HPSG data that indicates that an element should be
		 * combined with the element that follows, typically because an element
		 * is too large to be described by a single element.
		 */
		private static int PARTIAL_MASK = 1 << 7;

		/**
		 * Describes the reachability/solidity of the element. Must be set to
		 * one of the SOLIDITY_* values.
		 */
		private int mSolidity;

		/**
		 * Describes the type/kind of the element. Must be set to one of the
		 * KIND_* values.
		 */
		private int mKind;

		/**
		 * Describes the length of the element, in bytes.
		 */
		private int mLength;

		/**
		 * Creates an uninitialized element.
		 */
		public HeapSegmentElement() {
			setSolidity(SOLIDITY_INVALID);
			setKind(KIND_INVALID);
			setLength(-1);
		}

		/**
		 * Create an element describing the entry at the current position of
		 * hpsgData.
		 * 
		 * @param hs
		 *            The heap segment to pull the entry from.
		 * @throws BufferUnderflowException
		 *             if there is not a whole entry following the current
		 *             position of hpsgData.
		 * @throws ParseException
		 *             if the provided data is malformed.
		 */
		public HeapSegmentElement(HeapSegment hs)
				throws BufferUnderflowException, ParseException {
			set(hs);
		}

		/**
		 * Replace the element with the entry at the current position of
		 * hpsgData.
		 * 
		 * @param hs
		 *            The heap segment to pull the entry from.
		 * @return this object.
		 * @throws BufferUnderflowException
		 *             if there is not a whole entry following the current
		 *             position of hpsgData.
		 * @throws ParseException
		 *             if the provided data is malformed.
		 */
		public HeapSegmentElement set(HeapSegment hs)
				throws BufferUnderflowException, ParseException {

			/*
			 * TODO: Maybe keep track of the virtual address of each element so
			 * that they can be examined independently.
			 */
			ByteBuffer data = hs.mUsageData;
			int eState = (int) data.get() & 0x000000ff;
			int eLen = ((int) data.get() & 0x000000ff) + 1;

			while ((eState & PARTIAL_MASK) != 0) {

				/*
				 * If the partial bit was set, the next byte should describe the
				 * same object as the current one.
				 */
				int nextState = (int) data.get() & 0x000000ff;
				if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) {
					throw new ParseException("State mismatch", data.position());
				}
				eState = nextState;
				eLen += ((int) data.get() & 0x000000ff) + 1;
			}

			setSolidity(eState & 0x7);
			setKind((eState >> 3) & 0x7);
			setLength(eLen * hs.mAllocationUnitSize);

			return this;
		}

		public int getSolidity() {
			return mSolidity;
		}

		public void setSolidity(int solidity) {
			this.mSolidity = solidity;
		}

		public int getKind() {
			return mKind;
		}

		public void setKind(int kind) {
			this.mKind = kind;
		}

		public int getLength() {
			return mLength;
		}

		public void setLength(int length) {
			this.mLength = length;
		}

		public int compareTo(HeapSegmentElement other) {
			if (mLength != other.mLength) {
				return mLength < other.mLength ? -1 : 1;
			}
			return 0;
		}
	}

	// * The ID of the heap that this segment belongs to.
	protected int mHeapId;

	// * The size of an allocation unit, in bytes. (e.g., 8 bytes)
	protected int mAllocationUnitSize;

	// * The virtual address of the start of this segment.
	protected long mStartAddress;

	// * The offset of this pices from mStartAddress, in bytes.
	protected int mOffset;

	// * The number of allocation units described in this segment.
	protected int mAllocationUnitCount;

	// * The raw data that describes the contents of this segment.
	protected ByteBuffer mUsageData;

	// * mStartAddress is set to this value when the segment becomes invalid.
	private final static long INVALID_START_ADDRESS = -1;

	/**
	 * Create a new HeapSegment based on the raw contents of an HPSG chunk.
	 * 
	 * @param hpsgData
	 *            The raw data from an HPSG chunk.
	 * @throws BufferUnderflowException
	 *             if hpsgData is too small to hold the HPSG chunk header data.
	 */
	public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException {
		/*
		 * Read the HPSG chunk header. These get*() calls may throw a
		 * BufferUnderflowException if the underlying data isn't big enough.
		 */
		hpsgData.order(ByteOrder.BIG_ENDIAN);
		mHeapId = hpsgData.getInt();
		mAllocationUnitSize = (int) hpsgData.get();
		mStartAddress = (long) hpsgData.getInt() & 0x00000000ffffffffL;
		mOffset = hpsgData.getInt();
		mAllocationUnitCount = hpsgData.getInt();

		// Hold onto the remainder of the data.
		mUsageData = hpsgData.slice();
		mUsageData.order(ByteOrder.BIG_ENDIAN); // doesn't actually matter

		// Validate the data.
		// xxx do it
		// xxx make sure the number of elements matches mAllocationUnitCount.
		// xxx make sure the last element doesn't have P set
	}

	/**
	 * See if this segment still contains data, and has not been appended to
	 * another segment.
	 * 
	 * @return true if this segment has not been appended to another segment.
	 */
	public boolean isValid() {
		return mStartAddress != INVALID_START_ADDRESS;
	}

	/**
	 * See if <code>other</code> comes immediately after this segment.
	 * 
	 * @param other
	 *            The HeapSegment to check.
	 * @return true if <code>other</code> comes immediately after this segment.
	 */
	public boolean canAppend(HeapSegment other) {
		return isValid() && other.isValid() && mHeapId == other.mHeapId
				&& mAllocationUnitSize == other.mAllocationUnitSize
				&& getEndAddress() == other.getStartAddress();
	}

	/**
	 * Append the contents of <code>other</code> to this segment if it describes
	 * the segment immediately after this one.
	 * 
	 * @param other
	 *            The segment to append to this segment, if possible. If
	 *            appended, <code>other</code> will be invalid when this method
	 *            returns.
	 * @return true if <code>other</code> was successfully appended to this
	 *         segment.
	 */
	public boolean append(HeapSegment other) {
		if (canAppend(other)) {
			/*
			 * Preserve the position. The mark is not preserved, but we don't
			 * use it anyway.
			 */
			int pos = mUsageData.position();

			// Guarantee that we have enough room for the new data.
			if (mUsageData.capacity() - mUsageData.limit() < other.mUsageData
					.limit()) {
				/*
				 * Grow more than necessary in case another append() is about to
				 * happen.
				 */
				int newSize = mUsageData.limit() + other.mUsageData.limit();
				ByteBuffer newData = ByteBuffer.allocate(newSize * 2);

				mUsageData.rewind();
				newData.put(mUsageData);
				mUsageData = newData;
			}

			// Copy the data from the other segment and restore the position.
			other.mUsageData.rewind();
			mUsageData.put(other.mUsageData);
			mUsageData.position(pos);

			// Fix this segment's header to cover the new data.
			mAllocationUnitCount += other.mAllocationUnitCount;

			// Mark the other segment as invalid.
			other.mStartAddress = INVALID_START_ADDRESS;
			other.mUsageData = null;

			return true;
		} else {
			return false;
		}
	}

	public long getStartAddress() {
		return mStartAddress + mOffset;
	}

	public int getLength() {
		return mAllocationUnitSize * mAllocationUnitCount;
	}

	public long getEndAddress() {
		return getStartAddress() + getLength();
	}

	public void rewindElements() {
		if (mUsageData != null) {
			mUsageData.rewind();
		}
	}

	public HeapSegmentElement getNextElement(HeapSegmentElement reuse) {
		try {
			if (reuse != null) {
				return reuse.set(this);
			} else {
				return new HeapSegmentElement(this);
			}
		} catch (BufferUnderflowException ex) {
			/*
			 * Normal "end of buffer" situation.
			 */
		} catch (ParseException ex) {
			/*
			 * Malformed data.
			 */
			// TODO: we should catch this in the constructor
		}
		return null;
	}

	/*
	 * Method overrides for Comparable
	 */
	@Override
	public boolean equals(Object o) {
		if (o instanceof HeapSegment) {
			return compareTo((HeapSegment) o) == 0;
		}
		return false;
	}

	@Override
	public int hashCode() {
		return mHeapId * 31 + mAllocationUnitSize * 31 + (int) mStartAddress
				* 31 + mOffset * 31 + mAllocationUnitCount * 31
				+ mUsageData.hashCode();
	}

	@Override
	public String toString() {
		StringBuilder str = new StringBuilder();

		str.append("HeapSegment { heap ").append(mHeapId).append(", start 0x")
				.append(Integer.toHexString((int) getStartAddress()))
				.append(", length ").append(getLength()).append(" }");

		return str.toString();
	}

	public int compareTo(HeapSegment other) {
		if (mHeapId != other.mHeapId) {
			return mHeapId < other.mHeapId ? -1 : 1;
		}
		if (getStartAddress() != other.getStartAddress()) {
			return getStartAddress() < other.getStartAddress() ? -1 : 1;
		}

		/*
		 * If two segments have the same start address, the rest of the fields
		 * should be equal. Go through the motions, though. Note that we
		 * re-check the components of getStartAddress() (mStartAddress and
		 * mOffset) to make sure that all fields in an equal segment are equal.
		 */

		if (mAllocationUnitSize != other.mAllocationUnitSize) {
			return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1;
		}
		if (mStartAddress != other.mStartAddress) {
			return mStartAddress < other.mStartAddress ? -1 : 1;
		}
		if (mOffset != other.mOffset) {
			return mOffset < other.mOffset ? -1 : 1;
		}
		if (mAllocationUnitCount != other.mAllocationUnitCount) {
			return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1;
		}
		if (mUsageData != other.mUsageData) {
			return mUsageData.compareTo(other.mUsageData);
		}
		return 0;
	}
}
